﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization;
using JPR.Common;
using JPR.Common.Interfaces;
using JPR.TunelProxy.DataObjects;
using JPR.TunelProxy.Interfaces;
using System.Linq;

namespace JPR.TunelProxy.Proxy.Implementations
{
	public class ProxyWorker : IProxyWorker
	{
		#region Constants

		const string ContentLengthKey = "Content-Length";
		const string HostKey = "Host";
		const string ReadTimeoutKey = "ReadTimeout";

		#endregion

		#region Public fields

		public IClientDatagramProvider ClientDatagramProvider { get; set; }
		public IServerDatagramProvider ServerDatagramProvider { get; set; }
		public IFormatter Formatter { get; set; }
		public IHttpHeadersFormatter HeadersFormatter { get; set; }
		public IConfigProvider ConfigProvider { get; set; }
		public List<IDataProcessor> DataProcessors { get; set; }
	    public IHttpParser HttpParser { get; set; }

	    #endregion

		#region Private fields

		private HttpRequestData _requestData;
		private HttpResponseData _responseData;
		private long _contentLengthFromHttpHeaders;
		private long ContentLengthFromHttpHeaders
		{
			get
			{
				if (_contentLengthFromHttpHeaders < 0)
				{
					_contentLengthFromHttpHeaders = _requestData.HttpHeaders.ContainsKey(ContentLengthKey)
													? long.Parse(_requestData.HttpHeaders[ContentLengthKey])
													: -1;
				}
				return _contentLengthFromHttpHeaders;
			}
		}

		#endregion

		#region Constructors

	    public ProxyWorker()
	    {
            _requestData = new HttpRequestData();
            _responseData = new HttpResponseData();
            _contentLengthFromHttpHeaders = -1;
			DataProcessors = new List<IDataProcessor>();
	    }

	    public ProxyWorker(IClientDatagramProvider clientDatagramProvider, IServerDatagramProvider serverDatagramProvider,
	                       IFormatter formatter, IHttpHeadersFormatter headersFormatter, IConfigProvider configProvider,
	                       IHttpParser httpParser, params IDataProcessor[] dataProcessors) : this()
		{
			ClientDatagramProvider = clientDatagramProvider;
			ServerDatagramProvider = serverDatagramProvider;
			Formatter = formatter;
			HeadersFormatter = headersFormatter;
			ConfigProvider = configProvider;
            HttpParser = httpParser;
            DataProcessors = dataProcessors.ToList();
		}

		#endregion

		#region Public methods

		public void ProcessCommunication()
		{
			Stream data = ClientDatagramProvider.ReceiveData();
			ReadClientRequest(data);
			ApplyToServerHttpHeadersModification();
			Stream packedRequestStream = PackClientRequest();
			Stream transformedRequestStream = ApplyAllToServerTransformation(packedRequestStream);
			GetResponseFromServerAsync(transformedRequestStream);
		}

		#endregion

		#region Private methods

		private void ReadClientRequest(Stream data)
		{
			ParseRequestHeader(data);
			GetHostFromHttpHeaders();
			CopyRequestContent(data);
		}

		private void ParseRequestHeader(Stream data)
		{
			string requestHeaderLine = data.ReadLine();
			string[] commandLineArray = requestHeaderLine.Split(' ');
			_requestData.HttpCommand = (HttpCommand)Enum.Parse(typeof(HttpCommand), commandLineArray[0], true);
			_requestData.Object = commandLineArray[1];
			_requestData.Protocol = commandLineArray[2];
			do
			{
				requestHeaderLine = data.ReadLine();
				if (string.IsNullOrWhiteSpace(requestHeaderLine))
				{
					return;
				}
				string[] stringArray = requestHeaderLine.Split(':');
				_requestData.HttpHeaders.Add(stringArray[0].Trim(), stringArray[1].Trim());

			} while (true);
		}

		private void GetHostFromHttpHeaders()
		{
			if (_requestData.HttpHeaders.ContainsKey(HostKey))
			{
				_requestData.Host = _requestData.HttpHeaders[HostKey];
			}
			else
			{
				throw new InvalidDataException("Unable to parse Host from headers. Maybe obsolete format of GET command.");
			}
		}

		private void CopyRequestContent(Stream data)
		{
			Stopwatch sw = new Stopwatch();
			sw.Start();
			_requestData.Content = new byte[ContentLengthFromHttpHeaders];
			byte[] buffer = new byte[1024];
			long totalBytesCopied = 0;
			do
			{
				if (sw.ElapsedMilliseconds > ConfigProvider.GetIntValueByKey(ReadTimeoutKey))
				{
					throw new TimeoutException("Read from client request timeouted.");
				}
				int bytesReaded = data.Read(buffer, 0, buffer.Length);
				Array.Copy(buffer, 0,_requestData.Content,totalBytesCopied, bytesReaded);
				totalBytesCopied += bytesReaded;
			} while (totalBytesCopied < ContentLengthFromHttpHeaders);
			sw.Stop();
		}

		private void ApplyToServerHttpHeadersModification()
		{
			if (HeadersFormatter==null)
			{
				return;
			}
			_requestData.HttpHeaders = HeadersFormatter.ProcessHeadersToServer(_requestData.HttpHeaders);
		}

		private Stream PackClientRequest()
		{
			MemoryStream data = new MemoryStream();
			Formatter.Serialize(data, _requestData);
			return data;
		}

		private Stream ApplyAllToServerTransformation(Stream data)
		{
			Stream buffer = data;
			foreach (IDataProcessor dataProcessor in DataProcessors)
			{
				buffer = dataProcessor.ProcessToServer(buffer);
			}
			return buffer;
		}

		private void GetResponseFromServerAsync(Stream request)
		{
			ServerDatagramProvider.ReceiveDataAsync(request, GenericGetResponse);
		}

		private void GenericGetResponse(object sender, NetworkProviderEventArgs networkProviderEventArgs)
		{
            CreateClientResponseFromServerResponse(networkProviderEventArgs.Data);
			ApplyFromServerHttpHeadersModification();
			Stream data = CreateStreamFromHttpResponse();
			SendDataToClient(data);
		}

		private void CreateClientResponseFromServerResponse(Stream stream)
		{
            Stream data = ApplyAllFromServerTransformation(stream);
			_responseData = Formatter.Deserialize(data) as HttpResponseData;
		}

		private Stream ApplyAllFromServerTransformation(Stream data)
		{
			Stream buffer = data;
			foreach (IDataProcessor dataProcessor in DataProcessors.ToArray().Reverse())
			{
				buffer = dataProcessor.ProcessFromServer(buffer);
			}
			return buffer;
		}

		private Stream CreateStreamFromHttpResponse()
		{
			MemoryStream stream = new MemoryStream();
			foreach (KeyValuePair<string, string> header in _responseData.HttpHeaders)
			{
				WriteHeaderToStream(stream, header);
			}
			WriteContentToStream(stream, _responseData.Content);
			return stream;
		}

		private static void WriteHeaderToStream(Stream stream, KeyValuePair<string, string> httpHeader)
		{
			string headerLine = CreateHttpHeaderLine(httpHeader.Key, httpHeader.Value);
			stream.WriteLine(headerLine);
		}

		private static string CreateHttpHeaderLine(string key, string value)
		{
			string headerLine = key;
			if (!string.IsNullOrWhiteSpace(value))
			{
				headerLine = string.Concat(headerLine, ": ", value);
			}
			return headerLine;
		}

		private void WriteContentToStream(Stream stream, byte[] content)
		{
			if (!(content.Length > 0))
			{
				return;
			}
			stream.WriteLine();
			stream.Write(content, 0, content.Length);
		}

		private void ApplyFromServerHttpHeadersModification()
		{
			if (HeadersFormatter == null)
			{
				return;
			}
			_responseData.HttpHeaders = HeadersFormatter.ProcessHeadersFromServer(_responseData.HttpHeaders);
		}

		private void SendDataToClient(Stream data)
		{
			ClientDatagramProvider.SendData(data);
		}

		#endregion
	}
}